package gazzera.node.core.plugins.download;

import gazzera.node.core.peer.*;
import gazzera.utils.*;

import java.io.*;
import java.util.*;
import java.util.concurrent.atomic.*;


public class Download implements Serializable
{
   
	private static final long serialVersionUID = -6535687822142494317L;

	public static final int CHUNK_SIZE = 2*1024*1024;
	
	public static final AtomicInteger nextId = new AtomicInteger();
	
	public int id; // for the gui
    public FileID fileID;
    public String name;
    public long size;
    public long downloaded;
    public String subfolder;

    private RandomAccessFile f;
        
    private Map<Node, Integer> downloaders = new HashMap<Node, Integer>();
    
    private BitSet completedChunks;
    private BitSet downloading;
    private int[] chunkDownloaded;
    private File file = null;
    
    public long lastNotifiedTime;
    public long lastNotifiedSize;
    public int speed;
    
    public DownloadStatus phase;

    public Download()
    {
    	
    }
    
    public void save() throws IOException
    {
    	File f2 = new File(file.getAbsolutePath()+".extra");
    	DataOutputStream fos = new DataOutputStream(new FileOutputStream(f2));
    	try
		{
    		fos.writeInt(id);
			fos.writeUTF(name);
			FileIDUtils.write(fileID, fos);
			fos.writeUTF(subfolder);
			fos.writeLong(size);
			fos.writeLong(downloaded);
			
			for (int i = 0; i < chunkDownloaded.length; i++)
			{
				fos.writeInt(chunkDownloaded[i]);
			}
		}
		finally
		{
			fos.close();
		}
    }
    
    
    public Download(FileID fileID, String name, long size, File baseDir, String subfolder) throws IOException
    {
		File dir = new File(baseDir.getAbsolutePath()+File.separator+subfolder);
		dir.mkdirs();
		file = new File(dir, name);
			
		init(nextId.incrementAndGet(), fileID, name, size, file, subfolder, 0);
	
		for (int i = 0; i < getChunkCount(); i++)
		{
		    completedChunks.clear(i);
		    downloading.clear(i);
		    chunkDownloaded[i] = 0;
		}
	
		phase = DownloadStatus.DOWNLOADING;
	
		save();
    }

    private void init(int id, FileID fileID, String name, long size, File file, String subfolder, long downloaded) throws FileNotFoundException
    {
    	this.id = id;
	this.fileID = fileID;
	this.name = name;
	this.size = size;
	this.downloaded = downloaded;
	this.subfolder = subfolder;
	this.file = file;
	
	int num = getChunkCount();
	completedChunks = new BitSet(num);
	downloading = new BitSet(num);
	chunkDownloaded = new int[num];

	f = new RandomAccessFile(file, "rw");
    }

    public void load(File file) throws IOException
    {
	this.file = file;

	File f2 = new File(file.getAbsolutePath() + ".extra");
	DataInputStream fos = new DataInputStream(new FileInputStream(f2));
	try
	{
		int aid = fos.readInt();
	    String aname = fos.readUTF();
	    FileID aFileid = FileIDUtils.read(fos);
	    
	    String asubfolder = fos.readUTF();
	    long asize = fos.readLong();
	    long adownloaded = fos.readLong();

	    init(aid, aFileid, aname, asize, file, asubfolder, adownloaded);
	    
	    long checkTotal = 0;
	    
	    // restore chunk statuses
	    for (int i = 0; i < getChunkCount(); i++)
	    {
		chunkDownloaded[i] = fos.readInt();
		checkTotal += chunkDownloaded[i];
		//System.out.println(chunkDownloaded[i]);
		if (chunkDownloaded[i] == getChunkSize(i))
		{
		    completedChunks.set(i);
		}
		downloading.clear(i);
	    }

	    phase = DownloadStatus.DOWNLOADING;
	    
	    if (checkTotal != downloaded)
		throw new IOException("checkTotal != downloaded");

	    
	} finally
	{
	    fos.close();
	}
    }
    
    public void tick(long millis)
    {
    	long deltaSize = downloaded - lastNotifiedSize;
    	long deltaTime = millis - lastNotifiedTime;
    	if(deltaTime == 0) return;
    	lastNotifiedTime = millis;
    	lastNotifiedSize = downloaded;
    	if (isCompleted())
		{
    		// force to zero to avoid displayng stale speed of last seconds
    		speed = 0;
		}
    	else
    	{
    		speed = 1000*(int)( deltaSize / deltaTime );
    	}
    	
		
    }
    
    public Pair<Integer, Integer> getChunkToStart(BitSet avaiable)
    {
    	for (int i = 0; i < chunkDownloaded.length; i++)
		{
			if ( ! (completedChunks.get(i) || downloading.get(i) ))
			{
				// this chunk is neither downloading nor completed, use this!
				// return chunk index and partial for that chunk
				return new Pair<Integer, Integer>(i, chunkDownloaded[i]);
			}
		}
    	// not found
    	return null;
    	
    	
    }
    
	
	
	
	public void startChunk(Node node, int chunkIdx, int offset) throws Exception
	{
	    if(completedChunks.get(chunkIdx))
	    {
	    	throw new Exception("Chunk already completed! "+node.getHash()+" "+name+" "+chunkIdx);
	    }
	    if(downloading.get(chunkIdx))
	    {
	    	throw new Exception("Chunk already downloading! "+node.getHash()+" "+name+" "+chunkIdx);
	    }
	    if(chunkDownloaded[chunkIdx] != offset)
	    {
	    	throw new Exception("Chunk offset different "+node.getHash()+" "+name+" "+chunkIdx);
	    }
	    
	    downloading.set(chunkIdx);
	    
	    downloaders.put(node, chunkIdx);
	}
	
	
	
	public void chunkDownloadTerminated(Node node)
	{
	    int chunkIdx = downloaders.get(node);
	    downloaders.remove(node);
	    
	    downloading.clear(chunkIdx);
	    
	    if(chunkDownloaded[chunkIdx] == getChunkSize(chunkIdx))
	    {
			// completed!
			completedChunks.set(chunkIdx);
	    }
	    else
	    {
	    	completedChunks.clear(chunkIdx);
	    }
	    
	}
	
	/**
	 * 
	 * @param node
	 * @param data
	 * @return true if chunk finished
	 * @throws Exception
	 */
	public boolean writeData(Node node, byte[] data) throws Exception
	{
		int chunkIdx = downloaders.get(node);
		int chunkOffset = chunkDownloaded[chunkIdx];
		
		long realPos = chunkIdx*CHUNK_SIZE + chunkOffset;
		f.seek(realPos);
		f.write(data);
		
		downloaded += data.length;
		
		chunkDownloaded[chunkIdx] += data.length;
		int remaining = getChunkSize(chunkIdx)-chunkDownloaded[chunkIdx];
		if (remaining == 0)
		{
		    return true; // chunk finished
		}
		else if (remaining < 0)
		{
		    throw new IOException("Not syncronized! "+remaining+" "+node.getHash()+" "+name+" "+chunkIdx);
		}
		return false; // not finished yet 
	}
	
	public int getChunkCount()
	{
		int mod = (int)(size % CHUNK_SIZE);
		int num = (int)(size / CHUNK_SIZE);
		if(mod>0) num++;
		return num;
	}
	
	public int getChunkSize(int chunkIdx)
	{
		int mod = (int)(size % CHUNK_SIZE);
		int num = (int)(size / CHUNK_SIZE);
		
		if(chunkIdx<num)
		{
			return CHUNK_SIZE;
		}
		else
		{
			return mod;
		}
	}

	
	/**
	 * Close everything related to this download. After this call the download is safe to be dereferenced and forgotten
	 * @param complete if true, means the file is completed and must be moved to completeBaseDir
	 * @throws IOException 
	 */
    public File terminate(boolean complete, File completeBaseDir) throws IOException
    {
	phase = DownloadStatus.TERMINATED;
	File comp = null;
	try
	{
	    // clean the downloaders list. They're already eventually notified
	    // by the plugin
	    downloaders.clear();
	    // close main file writer
	    f.close();

	    if (complete)
	    {
		// move main file to complete folder
		
		// check if subfolder exists
		File compDir = new File(completeBaseDir.getAbsolutePath() + File.separator + subfolder);
		compDir.mkdirs();
		comp = new File(compDir, name);

		while (comp.exists())
		{
		    comp = new File(compDir, "bis." + comp.getName());
		}

		if (!getFile().renameTo(comp))
		{
		    phase = DownloadStatus.INCONSISTENT;
		    throw new IOException("Can't move file! :( from "+getFile().getAbsolutePath()+" to "+comp.getAbsolutePath());
		}

	    }
	    else
	    {
		// simply delete main file
		getFile().delete();
	    }
	    
	    // delete extra file
	    File extra = new File(getFile().getAbsolutePath() + ".extra");
	    extra.delete();
	}
	catch (IOException e)
	{
	    phase = DownloadStatus.INCONSISTENT;
	    throw e;
	}
	return comp;
    }
	
	
	public boolean isCompleted()
	{	
	    return completedChunks.cardinality() == getChunkCount();
	}
	public File getFile()
	{
		return file;
	}
	
	/*public static void main(String[] args) throws Exception
	{
		Download d = new Download(Hash.EMPTY, "test.avi", 1024*1024*10, null);
		int t = d.getChunkCount();
		for (int i = 0; i < t; i++)
		{
			System.out.println(i+" "+d.getChunkSize(i));
		}
	}
	*/
    
}
